﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace TribesPSO
{
    /// <summary>
    /// The Tribe class manages the links between particles.  A particle that does not belong to a tribe has no internal or external links
    /// </summary>
    public sealed class Tribe//<TParticle> where TParticle : Particle
    {
        private double historicalBestError;
        private readonly IRandom rng;
        private readonly List<Particle> tribeMembers;
        private readonly List<Tribe> informers;

        /// <summary>
        /// Gets the best solution that the tribe has found so far
        /// </summary>
        public Solution BestSolution { get { return this.Shaman.BestSolution; } }

        /// <summary>
        /// Gets a read only collection of the particles that make up this tribe
        /// </summary>
        public IList<Particle> TribeMembers { get { return this.tribeMembers.AsReadOnly(); } }

        /// <summary>
        /// The Shaman is the best particle in the tribe
        /// </summary>
        public Particle Shaman
        {
            private set;
            get;
        }

        /// <summary>
        /// Gets the Shaman particles of other tribes that are informers of this tribe
        /// </summary>
        public IEnumerable<Particle> ExternalLinks { get { return this.informers.Select(o => (Particle)o.Shaman); } }

        /// <summary>
        /// Gets the number of internal links in the tribe.  This includes particle's self links and both outgoing and
        /// incoming links from a particle even if the endpoints of both links are the same.
        /// <example>
        /// A tribe of 2 particles will have four internal links.  Two self links and one link from particle A to particle B
        /// and another link from particle B to particle A
        /// </example>
        /// </summary>
        public int InternalLinkCount { get { return this.tribeMembers.Count() * this.tribeMembers.Count(); } }

        /// <summary>
        /// Gets the number of external links in the tribe  This only counts outgoing external links, not incoming links
        /// </summary>
        public int ExternalLinkCount { get { return this.informers.Count(); } }

        /// <summary>
        /// Indicates whether or not a tribe is "Good".  A tribe that has improved its best performance since the last adaptation
        /// has a 50% chance of being good.  A tribe that has not improved its best performance since the last adaptation is
        /// not good.  This value is recalculated every time the swarm adapts
        /// </summary>
        public bool IsGood
        {
            private set;
            get;
        }

        /// <summary>
        /// Gets the number of particles in the tribe
        /// </summary>
        public int MemberCount { get { return this.tribeMembers.Count(); } }

        /// <summary>
        /// Constructs a new tribe containing a single particle.
        /// </summary>
        /// <param name="member">The particle that will belong to this tribe.</param>
        /// <param name="parentSearchSpace">The search space that the tribe belongs to</param>
        /// <remarks>
        /// This constructor creates a new RNG for the tribe to use.  If you want to control the RNG used by the tribe,
        /// considering calling the Tribe(Particle,Random)
        /// </remarks>
        /// <exception cref="ArgumentNullException">Thrown if member is null</exception>
        public Tribe(Particle member) : this(member, new Random()) { }

        /// <summary>
        /// Constructs a new tribe containing a single particle.
        /// </summary>
        /// <param name="member">The particle that will belong to this tribe</param>
        /// <param name="randomNumberGenerator">Specifies an RNG for the tribe to use.</param>
        /// <param name="parentSearchSpace">The search space that the tribe belongs to</param>
        /// <exception cref="ArgumentNullException">Thrown if member is null or randomNumberGenerator is null</exception>
        public Tribe(Particle member, IRandom randomNumberGenerator) : this(Enumerable.Repeat(member, 1), randomNumberGenerator) { }

        /// <summary>
        /// Constructs a new tribe containing multiple particles
        /// </summary>
        /// <param name="members">An IEnumerable containing particles that will belong to this tribe.</param>
        /// <param name="parentSearchSpace">The search space that the tribe belongs to</param>
        /// <remarks>
        /// This constructor creates a new RNG for the tribe to use.  If you want to control the RNG used by the tribe, consider
        /// calling the Tribe(IEnumberable Particle, Random) constructor
        /// </remarks>
        /// <exception cref="ArgumentNullException">Thrown if members is null</exception>
        /// <exception cref="ArgumentException">Thrown if members is empty</exception>
        public Tribe(IEnumerable<Particle> members) : this(members, new Random()) { }

        /// <summary>
        /// Creates a new tribe containing multiple particles
        /// </summary>
        /// <param name="members">An IEnumerable containing the particles that will belong to this tribe</param>
        /// <param name="randomNumberGenerator">Specifies an RNG for the tribe to use.  It should be considered "owned" by the tribe object</param>
        /// <param name="parentSearchSpace">The search space that the tribe belongs to</param>
        /// <exception cref="ArgumentNullException">Thrown if members is null or randomNumberGenerator is null</exception>
        /// <exception cref="ArgumentException">Thrown if members is empty</exception>
        public Tribe(IEnumerable<Particle> members, IRandom randomNumberGenerator)
        {
            if (members == null)
                throw new ArgumentNullException("members");
            if (members.IsEmpty())
                throw new ArgumentException("members cannot be empty", "members");
            if (randomNumberGenerator == null) throw new ArgumentNullException("randomNumberGenerator");
            this.rng = randomNumberGenerator;
            this.tribeMembers = new List<Particle>(members);
            this.informers = new List<Tribe>();
            foreach (Particle p in this.tribeMembers)
            {
                if (p == null) throw new ArgumentNullException("members", "members cannot contain any null items");
                p.Parent = this;
            }
            UpdateShaman();
            this.historicalBestError = this.BestSolution.Error;
            UpdateIsGood();
        }

        /// <summary>
        /// Attempts to remove the worst particle from the tribe.  In the case of a monoparticle tribe,
        /// the removal will only occur if one of its informers has a better performance
        /// </summary>
        /// <returns>True if a particle was removed.  False if no particle was removed.
        /// The latter can happen in a monoparticle tribe if the particle has no better informers</returns>
        public bool TryRemoveWorstParticle()
        {
            bool removedParticle;

            if (this.tribeMembers.Count() > 1)
            {
                //This is a tribe with more than one particle.  We're just going to kill off the worst one
                var WorstTribeMember = this.tribeMembers.OrderByDescending(o => o.BestSolution.Error).First();
                this.tribeMembers.Remove(WorstTribeMember);
                removedParticle = true;
            }
            else if (this.tribeMembers.Count() == 1)
            {
                /* This is a monoparticle trice.  We need to see if we have any better informers
                 * If we don't, then we're saved from extinction because we still
                 * hold valuable information
                 */
                if (this.informers.Any(o => o.BestSolution.Error < this.BestSolution.Error))
                {
                    /*We're going to remove the last particle in this tribe.
                     * This will cause the tribe to go extinct so we need to redistribe this tribe's informers
                     */
                    Tribe BestInformerTribe = this.informers.OrderBy(o => o.BestSolution.Error).First();
                    Tribe.RedistributeLinks(this, BestInformerTribe);
                    this.tribeMembers.Clear();

                    removedParticle = true;
                    //This tribe is now dead.  The swarm is going to remove it from its tribe collection
                }
                else
                {
                    removedParticle = false;
                }
            }
            else
            {
                //There were zero particles in the tribe.  This shouldn't happen but I don't think we need to throw
                removedParticle = false;
            }

            return removedParticle;
        }

        /// <summary>
        /// Removes any external links from the source tribe and assigns them to the destination tribe
        /// </summary>
        /// <param name="source"></param>
        /// <param name="destination"></param>
        private static void RedistributeLinks(Tribe source, Tribe destination)
        {
            foreach (var informerTribes in source.informers)
            {
                informerTribes.RemoveInformer(source);
                informerTribes.AddInformer(destination);
                destination.AddInformer(informerTribes);
            }
            source.informers.Clear();
        }

        /// <summary>
        /// Adds a tribe to this tribe's list of informers
        /// </summary>
        /// <remarks>
        /// If the new informer is already an informer of this tribe, AddInformer will 
        /// return instead of adding the informer tribe a second time
        /// </remarks>
        /// <param name="informer"></param>
        public void AddInformer(Tribe informer)
        {
            if (informer == null) throw new ArgumentNullException("informer");
            if (this.informers.Contains(informer) || informer == this)
            {
                //If we already have this informer, do nothing
                //If the informer is us, do nothing.
                return;
            }
            else
            {
                this.informers.Add(informer);
                informer.AddInformer(this);
            }
        }

        private void RemoveInformer(Tribe source)
        {
            this.informers.Remove(source);
        }

        internal void NotifySwarmAdapted() //Internal because only the swarm needs to notify
        {
            UpdateIsGood();
        }

        /// <summary>
        /// Recalculates whether or not this tribe is "Good".  This is actually just a 50% probability but it remains consistent
        /// in between swarm adaptations
        /// </summary>
        private void UpdateIsGood()
        {
            if (this.BestSolution.Error < this.historicalBestError)
            {
                //At least one of the particles has improved it's best performance since the last adaptation
                this.IsGood = this.rng.NextDouble() >= .5;
                this.historicalBestError = this.BestSolution.Error;
            }
            else
            {
                this.IsGood = false;
            }

        }

        internal void NotifySwarmMoved() //Internal because only the swarm needs to notify
        {
            UpdateShaman();
        }

        /// <summary>
        /// Recalculates the shaman of the tribe
        /// </summary>
        private void UpdateShaman()
        {
            this.Shaman = this.tribeMembers.OrderBy(o => o.BestSolution.Error).First();
        }

        /// <summary>
        /// Returns a string that indicates the size of the tribe and the number of good particles in the tribe
        /// </summary>
        /// <returns>A string that represents the current state of the tribe</returns>
        public override string ToString()
        {
            return string.Format(CultureInfo.CurrentCulture,
                            "{0} Members - {1} are good",
                            this.tribeMembers.Count(),
                            this.tribeMembers.Count(o => o.IsGood));
        }
    }
}